package webmarket.jpacontrollers;

import java.io.Serializable;
import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityNotFoundException;
import javax.persistence.Persistence;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Expression;
import javax.persistence.criteria.Order;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;

import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;

import webmarket.common.Constants;
import webmarket.common.exception.NonexistentEntityException;

/**
 * 
 */
public class JpaControllerBase<T> implements Serializable {

	protected final Logger logger = LogManager.getLogger();

	private static final long serialVersionUID = 682109768149187953L;

	public JpaControllerBase(Class<T> _class) {
		this.emf = Persistence.createEntityManagerFactory(Constants.PERSISTENCE_UNIT_NAME);
		this._class = _class;
	}

	private Class<T> _class = null;
	protected EntityManagerFactory emf = null;

	public EntityManager getEntityManager() {
		return emf.createEntityManager();
	}

	/**
	 * Creates the entity in the database
	 * 
	 * @param entity
	 */
	public void create(T entity) {
		EntityManager em = null;
		try {
			em = getEntityManager();
			em.getTransaction().begin();
			em.persist(entity);
			em.getTransaction().commit();
		} catch (Throwable e) {
			logger.catching(Level.TRACE, e);
			throw e;
		} finally {
			if (em != null) {
				em.close();
			}
		}
	}

	/**
	 * Updates the given entity in the database
	 * 
	 * @param entity
	 * @throws NonexistentEntityException
	 * @throws Exception
	 */
	public void edit(T entity) {
		EntityManager em = null;

		em = getEntityManager();
		em.getTransaction().begin();
		entity = em.merge(entity);
		em.getTransaction().commit();
		em.close();
	}

	/**
	 * Deletes the given id's entity from the database
	 * 
	 * @param id
	 * @throws NonexistentEntityException
	 */
	public void destroy(Integer id) throws NonexistentEntityException {
		EntityManager em = null;
		try {
			em = getEntityManager();
			em.getTransaction().begin();
			T log;
			try {
				log = em.getReference(_class, id);
			} catch (EntityNotFoundException enfe) {
				throw new NonexistentEntityException("The log with id " + id + " no longer exists.", enfe);
			}
			em.remove(log);
			em.getTransaction().commit();
		} catch (Throwable e) {
			logger.catching(Level.TRACE, e);
			throw e;
		} finally {
			if (em != null) {
				em.close();
			}
		}
	}

	/**
	 * Lists all entities in the database
	 * 
	 * @return
	 */
	public List<T> findEntities() {
		return findEntities(true, -1, -1);
	}

	/**
	 * Gets entities in the database with given paging parameters
	 * 
	 * @param maxResults
	 * @param firstResult
	 * @return
	 */
	public List<T> findEntities(int maxResults, int firstResult) {
		return findEntities(false, maxResults, firstResult);
	}

	/**
	 * Gets entities in the database with given paging parameters
	 * 
	 * @param pagesize
	 * @param page
	 * @return
	 */
	public List<T> getPage(int pagesize, int page) {
		return findEntities(pagesize, (page - 1) * pagesize);
	}

	private List<T> findEntities(boolean all, int maxResults, int firstResult) {
		EntityManager em = getEntityManager();
		try {

			CriteriaQuery<T> cq = em.getCriteriaBuilder().createQuery(_class);
			cq.select(cq.from(_class));
			TypedQuery<T> q = em.createQuery(cq);
			if (!all) {
				q.setMaxResults(maxResults);
				q.setFirstResult(firstResult);
			}
			return q.getResultList();
		} catch (Throwable e) {
			logger.catching(Level.TRACE, e);
			throw e;
		} finally {
			em.close();
		}
	}

	/**
	 * Gets the given id's entity from the database
	 * 
	 * @param id
	 * @return
	 */
	public T find(Integer id) {
		EntityManager em = getEntityManager();
		try {
			return em.find(_class, id);
		} catch (Throwable e) {
			logger.catching(Level.TRACE, e);
			throw e;
		} finally {
			em.close();
		}
	}

	/**
	 * Gets the count of entites
	 * 
	 * @return
	 */
	public int getCount() {
		EntityManager em = getEntityManager();
		try {
			CriteriaQuery<Long> cq = em.getCriteriaBuilder().createQuery(Long.class);
			return getCount(emf.getCriteriaBuilder().conjunction(), em, cq, cq.from(_class));
		} catch (Throwable e) {
			logger.catching(Level.TRACE, e);
			throw e;
		} finally {
			em.close();
		}
	}

	/**
	 * Gets the count of entites with given WHERE Predicate
	 * 
	 * @param pred
	 * @param em
	 * @param cq
	 * @param rt
	 * @return
	 */
	protected int getCount(Predicate pred, EntityManager em, CriteriaQuery<Long> cq, Root<T> rt) {
		cq.getSelection();
		Expression<Long> count = em.getCriteriaBuilder().count(rt);
		cq.select(count.alias(count.getAlias()));
		cq.where(pred);
		Query q = em.createQuery(cq);
		return ((Long) q.getSingleResult()).intValue();

	}

	/**
	 * Gets all entites, ordered by given property
	 * 
	 * @param property
	 * @param descending
	 * @return
	 */
	public List<T> getOrdered(String property, boolean descending) {
		EntityManager em = getEntityManager();
		try {
			CriteriaQuery<T> cq = em.getCriteriaBuilder().createQuery(_class);
			cq.select(cq.from(_class));

			Root<T> root = cq.from(_class);
			Order order = getOrder(property, descending, em, root);
			cq.orderBy(order);
			TypedQuery<T> q = em.createQuery(cq);
			return q.getResultList();
		} catch (Throwable e) {
			logger.catching(Level.TRACE, e);
			throw e;
		} finally {
			em.close();
		}
	}

	protected Order getOrder(String property, boolean descending, EntityManager em, Root<T> root) {
		Path<Object> path = root.get(property);
		Order order = null;
		if (descending) {
			order = em.getCriteriaBuilder().desc(path);
		} else {
			order = em.getCriteriaBuilder().asc(path);
		}
		return order;
	}
}
